/*
 * ====================================================================
 * Copyright (C) 2021 AbandonedCart @ TagMo
 * ====================================================================
 */
package com.hiddenramblings.tagmo.nfctech

import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.nfc.FormatException
import android.nfc.Tag
import android.nfc.tech.*
import android.text.Editable
import androidx.documentfile.provider.DocumentFile
import com.hiddenramblings.tagmo.Preferences
import com.hiddenramblings.tagmo.R
import com.hiddenramblings.tagmo.TagMo
import com.hiddenramblings.tagmo.amiibo.Amiibo
import com.hiddenramblings.tagmo.amiibo.AmiiboFile
import com.hiddenramblings.tagmo.amiibo.AmiiboManager
import com.hiddenramblings.tagmo.amiibo.KeyManager
import com.hiddenramblings.tagmo.amiibo.tagdata.AmiiboData
import com.hiddenramblings.tagmo.eightbit.io.Debug
import com.hiddenramblings.tagmo.eightbit.os.Storage
import com.hiddenramblings.tagmo.eightbit.os.Version
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.BufferUnderflowException
import java.nio.ByteBuffer
import java.util.*

object TagArray {
    @JvmStatic
    fun Tag?.technology(): String {
        with (TagMo.appContext) {
            var type = getString(R.string.unknown_type)
            if (null == this@technology) return type
            techList.forEach {
                when {
                    MifareClassic::class.java.name == it -> {
                        type = when (MifareClassic.get(this@technology).type) {
                            MifareClassic.TYPE_CLASSIC -> getString(R.string.mifare_classic)
                            MifareClassic.TYPE_PLUS -> getString(R.string.mifare_plus)
                            MifareClassic.TYPE_PRO -> getString(R.string.mifare_pro)
                            else -> getString(R.string.mifare_classic)
                        }
                        return type
                    }
                    MifareUltralight::class.java.name == it -> {
                        type = when (MifareUltralight.get(this@technology).type) {
                            MifareUltralight.TYPE_ULTRALIGHT -> getString(R.string.mifare_ultralight)
                            MifareUltralight.TYPE_ULTRALIGHT_C -> getString(R.string.mifare_ultralight_c)
                            else -> getString(R.string.mifare_ultralight)
                        }
                        return type
                    }
                    NfcA::class.java.name == it -> { return getString(R.string.nfciso, "A") }
                    NfcB::class.java.name == it -> { return getString(R.string.nfciso, "B") }
                    IsoDep::class.java.name == it -> { return getString(R.string.isodep) }
                    Ndef::class.java.name == it -> { return getString(R.string.ndef) }
                    NdefFormatable::class.java.name == it -> { return getString(R.string.ndef_formatable) }
                }
            }
            return type
        }
    }

    private val mPrefs = Preferences(TagMo.appContext)
    @JvmStatic
    val NTAG215.isPowerTag: Boolean get() {
        if (mPrefs.powerTagEnabled()) {
            return this.transceive(NfcByte.POWERTAG_SIG)?.let {
                compareRange(it, NfcByte.POWERTAG_SIGNATURE, NfcByte.POWERTAG_SIGNATURE.size)
            } ?: false
        }
        return false
    }
    @JvmStatic
    val NTAG215.isElite: Boolean get() {
        if (mPrefs.eliteEnabled()) {
            val signature = this.readSignature(false)
            val page10 = hexToByteArray("FFFFFFFFFF")
            return signature?.let {
                compareRange(it, page10, 32 - page10.size, it.size)
            } ?: false
        }
        return false
    }

    private fun compareRange(data: ByteArray, data2: ByteArray?, offset: Int, len: Int): Boolean {
        var i = 0
        var j = offset
        while (j < len) {
            if (data2?.get(i) != data[j]) return false
            i++
            j++
        }
        return true
    }

    @JvmStatic
    fun compareRange(data: ByteArray, data2: ByteArray?, len: Int): Boolean {
        return compareRange(data, data2, 0, len)
    }

    @JvmStatic
    fun ByteArray.toHex(): String {
        val sb = StringBuilder()
        this.forEach { sb.append(String.format("%02X", it)) }
        return sb.toString()
    }

    fun ByteArray.toLong(): Long {
        val buffer = ByteBuffer.allocate(
            if (Version.isNougat) java.lang.Long.BYTES else 8
        ).put(this).also { it.flip() }
        return try {
            buffer.long
        } catch (bue: BufferUnderflowException) {
            try {
                buffer.int.toLong()
            } catch (bue: BufferUnderflowException) {
                buffer.short.toLong()
            }
        }
    }

    fun ByteArray.toTagArray(): ByteArray {
        return this.copyOf(NfcByte.TAG_DATA_SIZE + 8)
    }

    fun ByteArray.toSignedArray(): ByteArray {
        return this.copyOf(NfcByte.TAG_FULL_SIZE)
    }

    fun ByteArray?.toDecryptedTag(keyManager: KeyManager): ByteArray {
        return try {
            keyManager.decrypt(this)
        } catch (e: Exception) {
            keyManager.decrypt(getValidatedData(keyManager, this))
        }
    }

    fun ByteArray.toPages(): Array<ByteArray?> {
        val pages = arrayOfNulls<ByteArray>(this.size / NfcByte.PAGE_SIZE)
        var i = 0
        var j = 0
        while (i < this.size) {
            pages[j] = this.copyOfRange(i, i + NfcByte.PAGE_SIZE)
            i += NfcByte.PAGE_SIZE
            j++
        }
        return pages
    }

    suspend fun ByteArray?.withRandomSerials(count: Int, keyManager: KeyManager? = null) : ArrayList<AmiiboData> {
        val dataList: ArrayList<AmiiboData> = arrayListOf()
        (keyManager?.decrypt(this) ?: this)?.let { tagData ->
            coroutineScope { (0 until count).map { async(Dispatchers.IO) {
                try {
                    dataList.add(AmiiboData(tagData.clone()).apply {
                        uID = Foomiibo.generateRandomUID()
                    })
                } catch (e: Exception) {
                    Debug.warn(e)
                }
            } }.awaitAll() }
        }
        return dataList
    }

    fun Long.toByteArray(): ByteArray {
        return ByteBuffer.allocate(
                if (Version.isNougat) java.lang.Long.BYTES else 8
        ).putLong(this).array()
    }

    @JvmStatic
    fun hexToByteArray(hex: String): ByteArray {
        val len: Int = hex.length
        val data = ByteArray(len / 2)
        var i = 0
        while (i < len) {
            data[i / 2] = ((Character.digit(hex[i], 16) shl 4)
                    + Character.digit(hex[i + 1], 16)).toByte()
            i += 2
        }
        return data
    }

    fun String.toHexByteArray() : ByteArray {
        return hexToByteArray(this.filter { !it.isWhitespace() })
    }

    fun hexToString(hex: String): String {
        val output = StringBuilder()
        var i = 0
        while (i < hex.length) {
            try {
                output.append(Integer.parseInt(hex.substring(i, i + 2), 16).toChar())
            } catch (nf: NumberFormatException) {
                Debug.warn(nf)
                output.clear()
                break
            }
            i += 2
        }
        return output.toString()
    }

    fun hexToLong(hex: String): Long {
        var result: Long = 0
        try {
            result = hex.toLong(16)
        } catch (nf: NumberFormatException) {
            hex.forEach { result = (result shl 4) + Character.digit(it, 16).toLong() }
        }
        return result
    }

    @JvmStatic
    @Throws(Exception::class)
    fun validateData(data: ByteArray?): ByteArray {
        with (TagMo.appContext) {
            if (null == data) throw IOException(getString(R.string.invalid_data_null))
            /* TagWriter.splitPages(data) */
            if (data.size == NfcByte.KEY_FILE_SIZE || data.size == NfcByte.KEY_RETAIL_SZ)
                throw IOException(getString(R.string.invalid_tag_key))
            else if (data.size < NfcByte.TAG_DATA_SIZE)
                throw IOException(getString(R.string.invalid_data_size, data.size, NfcByte.TAG_DATA_SIZE))
            val pages = data.toPages()
            when {
                pages[0]?.let {
                    it[0] != 0x04.toByte()
                } ?: true -> throw Exception(getString(R.string.invalid_tag_prefix))

                pages[2]?.let {
                    it[2] != 0x0F.toByte() || it[3] != 0xE0.toByte()
                } ?: true -> throw Exception(getString(R.string.invalid_tag_lock))

                pages[3]?.let {
                    it[0] != 0xF1.toByte() || it[1] != 0x10.toByte()
                            || it[2] != 0xFF.toByte() || it[3] != 0xEE.toByte()
                } ?: true -> throw Exception(getString(R.string.invalid_tag_cc))

                pages[0x82]?.let {
                    it[0] != 0x01.toByte() || it[1] != 0x0.toByte() || it[2] != 0x0F.toByte()
                } ?: true -> throw Exception(getString(R.string.invalid_tag_dynamic))

                pages[0x83]?.let {
                    it[0] != 0x0.toByte() || it[1] != 0x0.toByte()
                            || it[2] != 0x0.toByte() || it[3] != 0x04.toByte()
                } ?: true -> throw Exception(getString(R.string.invalid_tag_cfg_zero))

                pages[0x84]?.let {
                    it[0] != 0x5F.toByte() || it[1] != 0x0.toByte()
                            || it[2] != 0x0.toByte() || it[3] != 0x00.toByte()
                } ?: true -> throw Exception(getString(R.string.invalid_tag_cfg_one))

                else -> {}
            }
            return data
        }
    }

    @JvmStatic
    @Throws(Exception::class)
    fun validateNtag(mifare: NTAG215, tagData: ByteArray?, validateNtag: Boolean) {
        with (TagMo.appContext) {
            if (null == tagData) throw IOException(getString(R.string.no_source_data))
            if (validateNtag) {
                try {
                    mifare.transceive(byteArrayOf(0x60.toByte()))?.let {
                        if (it.size != 8) throw Exception(getString(R.string.error_tag_version))
                        if (it[0x02] != 0x04.toByte() || it[0x06] != 0x11.toByte())
                            throw FormatException(getString(R.string.error_tag_specs))
                    } ?: throw Exception(getString(R.string.error_tag_version))
                } catch (e: Exception) {
                    Debug.warn(R.string.error_version, e)
                    throw e
                }
            }
            val pages = mifare.readPages(0)
            if (null == pages || pages.size != NfcByte.PAGE_SIZE * 4)
                throw Exception(getString(R.string.fail_read_size))
            if (!compareRange(pages, tagData, 9))
                throw Exception(getString(R.string.fail_mismatch_uid))
            Debug.verbose(TagWriter::class.java, R.string.validation_success)
        }
    }

    @JvmStatic
    fun decipherFilename(amiibo: Amiibo, tagData: ByteArray?, verified: Boolean): String {
        var status = ""
        if (verified) {
            status = try {
                validateData(tagData)
                "Validated"
            } catch (e: Exception) {
                Debug.warn(e)
                Debug.getExceptionClass(e)
            }
        }
        try {
            val name = amiibo.name?.replace(File.separatorChar, '-')
            val uidHex = (tagData?.copyOfRange(0, 9))?.toHex()
            return if (verified) String.format(
                Locale.ROOT, "%1\$s[%2\$s]-%3\$s", name, uidHex, status
            ) else String.format(
                Locale.ROOT, "%1\$s[%2\$s]", name, uidHex
            )
        } catch (ex: Exception) {
            Debug.warn(ex)
        }
        return ""
    }

    @JvmStatic
    fun decipherFilename(
        amiiboManager: AmiiboManager?, tagData: ByteArray?, verified: Boolean
    ): String {
        try {
            amiiboManager?.let {
                return it.amiibos[Amiibo.dataToId(tagData)]?.let { amiibo ->
                    decipherFilename(amiibo, tagData, verified)
                } ?: ""
            }
        } catch (ex: Exception) {
            Debug.warn(ex)
        }
        return ""
    }

    @JvmStatic
    @Throws(Exception::class)
    fun getValidatedData(keyManager: KeyManager, data: ByteArray?): ByteArray {
        if (null == data) throw Exception(TagMo.appContext.getString(R.string.no_source_data))
        var validated = try {
            validateData(data)
        } catch (e: Exception) {
            validateData(keyManager.encrypt(data))
        }
        validated = keyManager.decrypt(validated)
        return keyManager.encrypt(validated)
    }

    @JvmStatic
    @Throws(Exception::class)
    fun getValidatedFile(keyManager: KeyManager, file: File): ByteArray {
        return getValidatedData(keyManager, TagReader.readTagFile(file))
    }

    @Throws(Exception::class)
    fun getValidatedDocument(keyManager: KeyManager, fileUri: Uri): ByteArray {
        return getValidatedData(keyManager, TagReader.readTagDocument(fileUri))
    }

    @Throws(Exception::class)
    fun getValidatedDocument(keyManager: KeyManager, file: DocumentFile): ByteArray {
        return getValidatedData(keyManager, TagReader.readTagDocument(file.uri))
    }

    @JvmStatic
    @Throws(Exception::class)
    fun getValidatedAmiibo(keyManager: KeyManager, file: AmiiboFile): ByteArray? {
        return file.data?.let { getValidatedData(keyManager, it) }
            ?: file.docUri?.let { getValidatedDocument(keyManager, it) }
            ?: file.filePath?.let { getValidatedFile(keyManager, it) }
    }

    @JvmStatic
    @Throws(IOException::class)
    fun writeBytesToFile(directory: File?, name: String, tagData: ByteArray?): String {
        val binFile = File(directory, "$name.bin")
        FileOutputStream(binFile).use { it.write(tagData) }
        try {
            MediaScannerConnection.scanFile(
                TagMo.appContext, arrayOf(binFile.canonicalPath), null, null
            )
        } catch (_: Exception) { }
        return binFile.canonicalPath
    }

    @JvmStatic
    @Throws(IOException::class)
    fun writeBytesToDocument(
        context: Context, directory: DocumentFile, name: String, tagData: ByteArray?
    ): Uri? {
        // displayName – name of new document, without any file extension appended; the underlying provider may choose to append the extension
        // The underlying provider does NOT provide an extension, therefore one IS appended
        val newFile = directory.createFile(
            context.resources.getStringArray(R.array.mimetype_bin)[0], "$name.bin"
        )
        newFile?.let { file ->
            context.contentResolver.openOutputStream(file.uri).use { it?.write(tagData) }
        }
        return newFile?.uri
    }

    fun writeBytesWithName(context: Context, fileName: String?, directory: String, tagData: ByteArray?) : String? {
        return with (Preferences(context.applicationContext)) {
            fileName?.let { name ->
                if (isDocumentStorage) {
                    val rootDocument = browserRootDocument()?.let { uri ->
                        DocumentFile.fromTreeUri(context, Uri.parse(uri))
                    } ?: throw NullPointerException()
                    writeBytesToDocument(context, rootDocument, name, tagData)?.toString()
                } else {
                    val destination = Storage.getDownloadDir("TagMo", directory)
                    destination.mkdirs()
                    writeBytesToFile(destination, name, tagData)
                }
            }
        }
    }

    fun writeBytesWithName(context: Context, input: Editable?, tagData: ByteArray?) : String? {
        return writeBytesWithName(context, input?.toString(), "Backups", tagData)
    }
}